package config

import (
	"log/slog"
	"path/filepath"
	"runtime"
	"sync"
	"time"

	"github.com/fsnotify/fsnotify"
)

// OnChange 监听配置文件修改, 触发回调函数
//   - 注意: Windows平台 fsnotify 有触发两次Bug
func OnChange(fn func(e fsnotify.Event)) {
	defaultConfig.OnChange(fn)
}
func (c *config) OnChange(fn func(e fsnotify.Event)) {
	defaultMu.Lock()
	defer defaultMu.Unlock()
	if fn == nil {
		return
	}
	c.initWatcher()
	if c.watcher == nil {
		GetLogger().Warn("config:OnChanges, init fsnotify.watcher failed")
		return
	}
	for file := range c.files {
		dir := filepath.Dir(file)
		if _, ok := c.watchDirs[dir]; !ok {
			c.watchDirs[dir] = struct{}{}
			if err := c.watcher.Add(dir); err != nil {
				GetLogger().Warn("config:OnChanges, add config file to fsnotify.watcher failed", slog.String("dir", dir), slog.String("error", err.Error()))
			}
		}
	}
	c.onChanges = append(c.onChanges, fn)
}
func (c *config) initWatcher() {
	if c.watcher != nil {
		return
	}
	c.watchOnce.Do(func() {
		watcher, err := fsnotify.NewWatcher()
		if err != nil {
			GetLogger().Warn("config:initWatcher, create fsnotify.watcher failed", slog.String("error", err.Error()))
			return
		}
		c.watcher = watcher
		initWG := sync.WaitGroup{}
		initWG.Add(1)
		go func() {
			defer c.watcher.Close()
			eventsWG := sync.WaitGroup{}
			eventsWG.Add(1)
			go func() {
				for {
					select {
					case event, ok := <-watcher.Events:
						if !ok { // 'Events' channel is closed
							GetLogger().Debug("config:Watcher, 'Events' channel is closed")
							eventsWG.Done()
							return
						}
						eventFile := filepath.Clean(event.Name)
						// 是否来自已加载的配置的文件
						if _, ok := c.files[eventFile]; !ok {
							continue
						}
						// Windows平台 fsnotify 有触发两次Bug
						if runtime.GOOS == "windows" {
							// 1秒内仅触发一次事件
							unixTime := time.Now().Unix() - 1
							if unixTime <= c.files[eventFile] {
								GetLogger().Debug("config:Watcher, the file events duplicate.", slog.String("file", eventFile))
								continue
							}
							c.files[eventFile] = unixTime
						}
						// if the config file was write/ereate/removed
						if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) {
							GetLogger().Debug("config:Watcher, the file changed.", slog.String("file", eventFile))
							for file := range c.files {
								fileExt := filepath.Ext(file)
								if len(fileExt) < 2 {
									GetLogger().Warn("config:Watcher, the file not has ext", slog.String("file", file))
									continue
								}
								deafult().SetConfigFile(file)
								deafult().SetConfigType(fileExt[1:])
								if err := deafult().MergeInConfig(); err != nil {
									GetLogger().Warn("config:Watcher, the file read failed.", slog.String("file", file), slog.String("error", err.Error()))
								}
							}
							for _, v := range c.onChanges {
								go v(event)
							}
						} else if event.Has(fsnotify.Remove) {
							GetLogger().Debug("config:Watcher, the file[%s] removed.", slog.String("file", eventFile))
							if len(c.files) == 1 {
								continue
							}
							for file := range c.files {
								if file != eventFile {
									fileExt := filepath.Ext(file)
									if len(fileExt) < 2 {
										GetLogger().Warn("config:Watcher, the file not has ext", slog.String("file", file))
										continue
									}
									deafult().SetConfigFile(file)
									deafult().SetConfigType(fileExt[1:])
									if err := deafult().MergeInConfig(); err != nil {
										GetLogger().Warn("config:Watcher, the file read failed.", slog.String("file", file), slog.String("error", err.Error()))
									}
								}
							}
							for _, v := range c.onChanges {
								go v(event)
							}
						}
					case err, ok := <-watcher.Errors:
						if ok { // 'Errors' channel is not closed
							GetLogger().Warn("config:Watcher, 'Errors' channel is not closed", slog.String("error", err.Error()))
						}
						eventsWG.Done()
						return
					}
				}
			}()
			initWG.Done()   // done initializing the watch in this go routine, so the parent routine can move on...
			eventsWG.Wait() // now, wait for event loop to end in this go-routine...
		}()
		initWG.Wait() // make sure that the go routine above fully ended before returning
	})
}
